<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\utils;

/**
 * Utility class for i18n-related operations. Switcher's internationalization relies on
 * ISO 639-1 language codes and ISO 3166-1 alpha-2 country codes expected in HTTP accept
 * headers.
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\utils
 * @since 1.0.0
 */
final class InternationalizationHelper
{
	/**
	 * Regular expression for an ISO 639-1 language code.
	 * @var string
	 */
	const LANG_REGEXP = '/^[a-z]{2}$/';
	
	/**
	 * Regular expression for an ISO 639-1 language code/ISO 3166-1 alpha-2 country code pair.
	 * @var string
	 */
	const LANG_COUNTRY_REGEXP = '/^[a-z]{2}(-[a-z]{2})?$/';
	
	/**
	 * The registered language which higher priority than accepted languages.
	 * @var string
	 */
	private static $registeredLanguage = null;
	
	/**
	 * Hidden constructor
	 */
	private function __construct()
	{
	}
	
	/**
	 * Register a language with a higher priority than HTTP accepted languages.
	 * 
	 * @param string $language The language to register
	 * @throws \InvalidArgumentException If the language is invalid
	 */
	public static function registerLanguage($language)
	{
		if ( !preg_match(self::LANG_COUNTRY_REGEXP, $language) )
		{
			throw new \InvalidArgumentException('Invalid language : ' . $language);
		}
		
		self::$registeredLanguage = $language;
	}
	
	/**
	 * Remove the registered language.
	 */
	public static function unregisterLanguage()
	{
		self::$registeredLanguage = null;
	}
	
	/**
	 * Find the user preferred language among supported languages.
	 * 
	 * @param array $supportedLanguages The supported languages
	 * @return mixed The preferred supported language or false
	 * @see HttpHelper::getAcceptedLanguages
	 */
	public static function negociateLanguage(array $supportedLanguages)
	{
		$acceptedLanguages = HttpHelper::getAcceptedLanguages();
		self::putRegisteredLanguage($acceptedLanguages);
		
		foreach ($acceptedLanguages as $lang)
		{
			if (array_search($lang, $supportedLanguages) !== false)
			{
				return $lang;
			}
		}
		
		return false;
	}
	
	/**
	 * Add the registered language with a higher priority than accepted languages.
	 * 
	 * @param array $acceptedLanguages The accepted languages
	 */
	private static function putRegisteredLanguage(array &$acceptedLanguages)
	{
		if (self::$registeredLanguage != null)
		{
			array_unshift($acceptedLanguages, self::$registeredLanguage);
		}
	}
	
	/**
	 * Load a resource bundle for the preferred lang and country. Resource bundles
	 * are INI files with localization defined into sections. They must follow the
	 * syntax of standard PHP INI files. The default language is defined in the
	 * <i>default</i> section of the resource bundle.
	 * 
	 * When a resource bundle is loaded, it contains the default values, optionally
	 * overriden by a specific lang's values, optionally overriden by a specific
	 * lang-country pair's values. For consistency sake, the default lang should
	 * contain default values for every possible keys.
	 * 
	 * <p>Resource bundle example :
	 * <code>
	 * ; This is a localized resource bundle
	 * ; Comments start with a semicolon ';', as in php.ini
	 * 
	 * [default]
	 * lang = "en"
	 * 
	 * [en]
	 * titleSuffix = " - MyWebSite"
	 * ok= "OK"
	 * cancel= "Cancel"
	 * coyrightNotice = "Copyright © MyWebSite - All rights reserved"
	 * 
	 * [fr]
	 * cancel= "Annuler"
	 * coyrightNotice = "Copyright © MyWebSite - Tous droits réservés"
	 * 
	 * [fr-ca]
	 * cancel= "Canceller"
	 * </code>
	 * 
	 * The following code loads and display the bundle :
	 * <code>
	 * var_dump( InternationalizationHelper::loadBundle('example.ini') );
	 * </code>
	 * 
	 * Output for a user agent with higher priority on fr-ca :
	 * <code>
	 * array
	 *   'titleSuffix' => string ' - MyWebSite' (length=12)
	 *   'ok' => string 'OK' (length=2)
	 *   'cancel' => string 'Canceller' (length=9)
	 *   'coyrightNotice' => string 'Copyright © MyWebSite - Tous droits réservés' (length=47)
	 * </code>
	 * </p>
	 * 
	 * @param string $ini The path to the INI file
	 * @return array An associative array of key/value resources
	 * @throws \InvalidArgumentException If the INI file doesn't exist
	 * @throws \InvalidArgumentException If the bundle isn't valid
	 */
	public static function loadBundle($ini)
	{
		$sections = self::loadBundleSections($ini);
		$defaultLang = $sections['default']['lang'];
		$negociatedLang = self::negociateLanguage( array_keys($sections) );
		
		if ($negociatedLang === false)
		{
			return $sections[$defaultLang];
		}
		
		if (strlen($negociatedLang) == 2)
		{
			return array_merge($sections[$defaultLang], $sections[$negociatedLang]);
		}
		
		$lang = substr($negociatedLang, 0, 2);
		
		if ( array_key_exists($lang, $sections) )
		{
			$bundle = array_merge($sections[$defaultLang], $sections[$lang]);
			return array_merge($bundle, $sections[$negociatedLang]);
		}
		
		return array_merge($sections[$defaultLang], $sections[$negociatedLang]);
	}
	
	/**
	 * Load and validate an INI file.
	 * 
	 * @param string $ini The bundle file name
	 * @return array The sections of the INI bundle
	 * @throws \InvalidArgumentException If the INI file doesn't exist
	 * @throws \InvalidArgumentException If the bundle isn't valid
	 */
	private static function loadBundleSections($ini)
	{
		$sections = @parse_ini_file($ini, true);
		
		if ($sections === false)
		{
			throw new \InvalidArgumentException('Missing bundle file : ' . $ini);
		}
		
		self::validateBundleSections($ini, $sections);
		
		return $sections;
	}
	
	/**
	 * Validate a loaded INI file. Check the default section and make sure a section
	 * exists for the default language. Other languages are ignored.
	 * 
	 * @param $sections The sections of the resource bundle
	 * @throws \InvalidArgumentException If a default lang isn't declared properly
	 */
	private static function validateBundleSections($ini, $sections)
	{
		if ( !array_key_exists('default', $sections) )
		{
			throw new \InvalidArgumentException('Missing default section : ' . $ini);
		}
		
		if (!array_key_exists('lang', $sections['default']) || count($sections['default']) != 1)
		{
			throw new \InvalidArgumentException('Illegal default section : ' . $ini);
		}
		
		$defaultLang = $sections['default']['lang'];
		
		if ( !array_key_exists($defaultLang, $sections) || !preg_match(self::LANG_REGEXP, $defaultLang) )
		{
			throw new \InvalidArgumentException('Illegal default lang : ' . $defaultLang);
		}
	}
}

?>